﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Threading;
using ScrumTable.Common;
using ScrumTable.Common.FileSystem;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Reflection;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Common.Config.Driver;
using ScrumTable.DL.Data.Common.Config.Schema;
using ScrumTable.DL.Data.Properties;

#endregion


namespace ScrumTable.DL.Data
{
    /// <summary>
    /// The DataAssembly class contains information about a plug in assembly.
    /// </summary>
    /// <remarks>
    /// Caution:
    /// The use of this property IS NOT MULTI-THREADING SAVE!
    /// DO NOT CHANGE ANY DATA OUTSIDE FROM THE GUI THREAD!
    /// </remarks>
    public class DataAssembly : IDataAssemblyInfo
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string DllFilter = "*.dll";
        private const string DriverFolder = "Drivers";

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the description of the data assembly. This property may be
        /// used in a UI drop down list in order to select a proprietary
        /// data connector plug in.
        /// </summary>
        public string Description { get; private set; }

        /// <summary>
        /// Returns true if the data assembly reqiures strong username / password authentication.
        /// </summary>
        public bool AssemblyRequiresStrongAuthentication { get; private set; }

        /// <summary>
        /// Specifies the data assembly format for the driver data source link (passed to the Connect()).
        /// </summary>
        public DataSourceLinkFormat AssemblyRequiredLinkFormat { get; private set; }

        /// <summary>
        /// Returns the file extension if the RequiredLinkFormat is set to
        /// FilePath.
        /// </summary>
        public string AssemblyRequiredDataExtension { get; private set; }

        /// <summary>
        /// Gets the image if available of the data assembly. 
        /// </summary>
        public System.Drawing.Bitmap AssemblyBitmap { get; private set; }

        /// <summary>
        /// Gets the schema data sources associated to the current data assembly and
        /// its data driver.
        /// </summary>
        public IDataSchemaSelector DataSchemaSelector { get; private set; }

        /// <summary>
        /// Gets the configuration data sources associated to the current data assembly and
        /// its data driver.
        /// </summary>
        public IDriverConfigSelector DriverConfigSelector { get; private set; }

        /// <summary>
        /// Gets the user specific configuration folder path for the current application.
        /// </summary>
        public string ConfigFolderPath
        {
            get { return GetPlugInConfigFolder().FullName; }
        }

        /// <summary>
        /// Gets the user specific data folder path for the current application.
        /// </summary>
        public string DataFolderPath
        {
            get { return GetPlugInDataFolder().FullName; }
        }

        private DataAssemblyAttribute DataAssemblyInfo { get; set; }

        private IDataAssemblyDriverActivator DataAssemblyDriverActivator { get; set; }
        private Assembly DataAssemblyDefinition { get { return DataAssemblyInfo.DataAssemblyDriverActivatorType.Assembly; } }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        private DataAssembly(DataAssemblyAttribute attribute, System.Drawing.Bitmap bitmap)
        {
            PreCondition.AssertNotNull(attribute, "attribute");

            DataAssemblyInfo = attribute;
            DataAssemblyDriverActivator = attribute.CreateDataAssemblyDriverActivator();

            AssemblyBitmap = bitmap;
            AssemblyRequiredLinkFormat = DataAssemblyDriverActivator.RequiredLinkFormat;
            AssemblyRequiresStrongAuthentication = DataAssemblyDriverActivator.RequiresStrongAuthentication;
            AssemblyRequiredDataExtension = DataAssemblyDriverActivator.RequiredDataExtension;
            Description = DataAssemblyDefinition.GetAssemblyDescription();
            DataSchemaSelector = DataAssemblyDriverActivator.CreateSchemaSelector(this);
            DriverConfigSelector = DataAssemblyDriverActivator.CreateDriverConfigSelector(this);
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format("{0} [{1}]", base.ToString(), DataAssemblyDefinition.FullName);
        }

        /// <summary>
        /// Maps a file path to a file which is stored on the underlying 
        /// hard drive in conjunction with the data file-system data retrieval rules.
        /// </summary>
        /// <param name="fileToMap">Specifies the file path which should be mapped through the plugin system.</param>
        /// <returns>Returns a mapped file path which points the a file on the file system.</returns>
        public string MapDataFilePath(string fileToMap)
        {
            PreCondition.AssertNotNullOrEmpty(fileToMap, "fileToMap");
            return MapFilePath(fileToMap, GetPlugInDataFolder);
        }

        /// <summary>
        /// Maps a folder path to a file which is stored on the underlying 
        /// hard drive in conjunction with the data driver file-system data retrieval rules.
        /// </summary>
        /// <param name="folderToMap">Specifies the file path which should be mapped through the plugin system.</param>
        /// <returns>Returns a mapped file path which points the a file on the file system.</returns>
        public string MapDataFolderPath(string folderToMap)
        {
            PreCondition.AssertNotNullOrEmpty(folderToMap, "folderToMap");
            return MapFolderPath(folderToMap, GetPlugInDataFolder);
        }

        /// <summary>
        /// Maps a file path to a file which is stored on the underlying 
        /// hard drive in conjunction with the config file-system data retrieval rules.
        /// </summary>
        /// <param name="fileToMap">Specifies the file path which should be mapped through the plugin system.</param>
        /// <returns>Returns a mapped file path which points the a file on the file system.</returns>
        public string MapConfigFilePath(string fileToMap)
        {
            PreCondition.AssertNotNullOrEmpty(fileToMap, "fileToMap");
            return MapFilePath(fileToMap, GetPlugInConfigFolder);
        }

        /// <summary>
        /// Maps a folder path to a file which is stored on the underlying 
        /// hard drive in conjunction with the config driver file-system data retrieval rules.
        /// </summary>
        /// <param name="folderToMap">Specifies the file path which should be mapped through the plugin system.</param>
        /// <returns>Returns a mapped file path which points the a file on the file system.</returns>
        public string MapConfigFolderPath(string folderToMap)
        {
            PreCondition.AssertNotNullOrEmpty(folderToMap, "folderToMap");
            return MapFolderPath(folderToMap, GetPlugInConfigFolder);
        }

        internal IDataContext InstantiateDataContext()
        {
            return DataAssemblyDriverActivator.CreateContext(new DataContextInfo(this));
        }

        private string MapFilePath(string fileToMap, Func<DirectoryInfo> plugInFolderFunctor)
        {
            if (Path.IsPathRooted(fileToMap))
            {
                // we cannot map an absolute path, so return the specified path
                return fileToMap;
            }
            else
            {
                DirectoryInfo dataAssemblyPlugInFolder = plugInFolderFunctor();
                string userDataBasedPath = Path.Combine(dataAssemblyPlugInFolder.FullName, fileToMap);
                string assemblyBasedPath = Path.Combine(DataAssemblyDefinition.Location, fileToMap);

                if (File.Exists(userDataBasedPath))
                {
                    return userDataBasedPath;
                }
                if (File.Exists(assemblyBasedPath))
                {
                    return assemblyBasedPath;
                }
                if (File.Exists(fileToMap))
                {
                    return fileToMap;
                }
                return userDataBasedPath;
            }
        }

        private string MapFolderPath(string folderToMap, Func<DirectoryInfo> plugInFolderFunctor)
        {
            if (Path.IsPathRooted(folderToMap))
            {
                // we cannot map an absolute path, so return the specified path
                return folderToMap;
            }
            else
            {
                DirectoryInfo dataAssemblyPlugInFolder = plugInFolderFunctor();
                string userDataBasedPath = Path.Combine(dataAssemblyPlugInFolder.FullName, folderToMap);
                string assemblyBasedPath = Path.Combine(DataAssemblyDefinition.Location, folderToMap);

                if (Directory.Exists(userDataBasedPath))
                {
                    return userDataBasedPath;
                }
                if (Directory.Exists(assemblyBasedPath))
                {
                    return assemblyBasedPath;
                }
                if (Directory.Exists(folderToMap))
                {
                    return folderToMap;
                }
                return userDataBasedPath;
            }
        }

        private DirectoryInfo GetPlugInDataFolder()
        {
            return DirectoryUtil.EnsureExistsFrom(
                Path.Combine(DataFolderUtil.GetDataPath(), DataAssemblyDefinition.GetName().Name),
                toEnsureExists =>
                    {
                        DataAssemblyDefinition.ExtractAssemblyResources(
                            ".Resources.Data",
                            toEnsureExists.FullName);
                    });
        }

        private DirectoryInfo GetPlugInConfigFolder()
        {
            return DirectoryUtil.EnsureExists(
                Path.Combine(DataFolderUtil.GetConfigPath(), DataAssemblyDefinition.GetName().Name));
        }

        #region Static Members For Simple Factory Method

        /// <summary>
        /// Loads a new driver data dassembly from the given assembly.
        /// </summary>
        /// <param name="loadedAssembly">Specifies the assembly to load.</param>
        /// <returns>Returns the assembly instance or a null pointer if the given assembly is not a valid ScrumTable data driver assembly.</returns>
        public static DataAssembly FromAssembly(Assembly loadedAssembly)
        {
            PreCondition.AssertNotNull(loadedAssembly, "loadedAssembly");

            var dataAssemblyAttribute = (from customAttrib in loadedAssembly.GetCustomAttributes(true)
                                         where (customAttrib.GetType() == typeof (DataAssemblyAttribute))
                                         select (customAttrib as DataAssemblyAttribute)).FirstOrDefault();

            if (dataAssemblyAttribute != null)
            {
                Logger<DataAssembly>.InfoFormat(Resources.LogDataLayerObjCreated, loadedAssembly.Location);

                return new DataAssembly(dataAssemblyAttribute, LoadBitmap(loadedAssembly));
            }
            return null;
        }

        /// <summary>
        /// Creates a list of new DataAssembly instances from assemblies 
        /// placed in the given directory.
        /// </summary>
        public static IList<DataAssembly> FromDirectory(string directoryPath)
        {
            PreCondition.AssertNotNullOrEmpty(directoryPath, "directoryPath");

            return LoadDataAssembliesFrom(new DirectoryInfo(directoryPath));
        }

        /// <summary>
        /// Creates a list of new DataAssembly instances from assemblies 
        /// placed in the given directory.
        /// </summary>
        internal static IList<DataAssembly> FromDriversDirectory(string directoryPath)
        {
            PreCondition.AssertNotNullOrEmpty(directoryPath, "directoryPath");

            List<DataAssembly> data = new List<DataAssembly>();
            DirectoryInfo directoryInfo = new DirectoryInfo(Path.Combine(directoryPath, DriverFolder));

            foreach (DirectoryInfo driverDirectory in directoryInfo.GetDirectories())
            {
                data.AddRange(LoadDataAssembliesFrom(driverDirectory));
            }
            return data;
        }

        private static IList<DataAssembly> LoadDataAssembliesFrom(DirectoryInfo parentDir)
        {
            List<DataAssembly> data = new List<DataAssembly>();

            if (parentDir.Exists)
            {
                foreach (FileInfo file in parentDir.GetFiles(DllFilter))
                {
                    DataAssembly plugInAttributeAssembly = LoadDataAssembly(file.FullName);

                    if (plugInAttributeAssembly != null)
                        data.Add(plugInAttributeAssembly);
                }
            }
            return data;
        }

        [DebuggerStepThrough]
        private static DataAssembly LoadDataAssembly(string assemblyPath)
        {
            try
            {
                return FromAssembly(Assembly.LoadFrom(assemblyPath));
            }
            catch (Exception e)
            {
                Logger<DataAssembly>.Info(Resources.LogDataAssLoadFailed, e);
            }
            return null;
        }

        private static System.Drawing.Bitmap LoadBitmap(Assembly assembly)
        {
            try
            {
                string manifestRes = (from resourceName in assembly.GetManifestResourceNames()
                                      where resourceName.Contains(".Properties.Resources")
                                      select resourceName).FirstOrDefault();

                if (!string.IsNullOrEmpty(manifestRes))
                {
                    ResourceManager iconResourceMngr = new ResourceManager(
                        manifestRes.Substring(0, manifestRes.LastIndexOf('.')), // cut off ".resources" at string end
                        assembly);

                    using (new DisposeAction(iconResourceMngr.ReleaseAllResources))
                    {
                        return (System.Drawing.Bitmap) iconResourceMngr.GetObject("Icon", CultureInfo.InvariantCulture);
                    }
                }
            }
            catch (Exception e)
            {
                Logger<DataAssembly>.Info(Resources.LogDataAssLoadFailed, e);
            }
            return null;
        }
        #endregion

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion
    }
}